#!/usr/bin/env python
# Required files (key3.db, signongs.sqlite, cert8.db)
# Inspired from https://github.com/Unode/firefox_decrypt/blob/master/firefox_decrypt.py

from ctypes import *
import sys, os, re, glob
from base64 import b64decode
from ConfigParser import RawConfigParser
import sqlite3
import json
import shutil
from lazagne.config.dico import get_dico
from itertools import product
#https://pypi.python.org/pypi/pyasn1/
from pyasn1.codec.der import decoder
from struct import unpack
from binascii import hexlify, unhexlify
from hashlib import sha1
import hmac
from Crypto.Util.number import long_to_bytes 
from Crypto.Cipher import DES3
from lazagne.config.constant import *
from lazagne.config.write_output import print_debug
from lazagne.config.moduleInfo import ModuleInfo

# Database classes
database_find = False
class Credentials(object):
	def __init__(self, db):
		global database_find
		self.db = db
		if os.path.isfile(db):
			# check if the database is not empty
			f = open(db, 'r')
			tmp = f.read()
			if tmp:
				database_find = True
			f.close()
	
	def __iter__(self):
		pass
	
	def done(self):
		pass

class JsonDatabase(Credentials):
	def __init__(self, profile):
		db = profile + os.sep + "logins.json"
		super(JsonDatabase, self).__init__(db)
	
	def __iter__(self):
		if os.path.exists(self.db):
			with open(self.db) as fh:
				data = json.load(fh)
				try:
					logins = data["logins"]
				except:
					raise Exception("Unrecognized format in {0}".format(self.db))
				
				for i in logins:
					yield (i["hostname"], i["encryptedUsername"],	i["encryptedPassword"])

class SqliteDatabase(Credentials):
	def __init__(self, profile):
		db = profile + os.sep + "signons.sqlite"
		super(SqliteDatabase, self).__init__(db)
		self.conn = sqlite3.connect(db)
		self.c = self.conn.cursor()
	
	def __iter__(self):
		self.c.execute("SELECT hostname, encryptedUsername, encryptedPassword FROM moz_logins")
		for i in self.c:
			yield i
	
	def done(self):
		super(SqliteDatabase, self).done()
		self.c.close()
		self.conn.close()


class Mozilla(ModuleInfo):
	# b = brute force attack
	# m = manually
	# d = default list
	# a = dictionary attack

	def __init__(self, isThunderbird = False):
		
		self.credentials_categorie = None	

		self.toCheck = []
		self.manually_pass = None
		self.dictionary_path = None
		self.number_toStop = None

		self.key3 = ''

		# Manage options
		suboptions = [
			{'command': '-m', 'action': 'store', 'dest': 'manually', 'help': 'enter the master password manually', 'title': 'Advanced Mozilla master password options'},
			{'command': '-s', 'action': 'store', 'dest': 'specific_path', 'help': 'enter the specific path to a profile you want to crack', 'title': 'Advanced Mozilla master password options'}
		]
		
		if not isThunderbird:
			options = {'command': '-f', 'action': 'store_true', 'dest': 'firefox', 'help': 'firefox'}
			ModuleInfo.__init__(self, 'firefox', 'browsers', options, suboptions)
		else:
			options = {'command': '-t', 'action': 'store_true', 'dest': 'thunderbird', 'help': 'thunderbird'}
			ModuleInfo.__init__(self, 'thunderbird', 'browsers', options, suboptions)
	
	def get_path(self, software_name):
		path = ''
		if software_name == 'Firefox':
			path = os.path.expanduser("~/.mozilla/firefox")
		elif software_name == 'Thunderbird':
			path = os.path.expanduser("~/.thunderbird")
		return path
	
	def manage_advanced_options(self):
		if constant.manually:
			self.manually_pass = constant.manually
			self.toCheck.append('m')
		
		if constant.path:
			self.dictionary_path = constant.path
			self.toCheck.append('a')
		
		if constant.bruteforce:
			self.number_toStop = int(constant.bruteforce) + 1
			self.toCheck.append('b')
		
		# default attack
		if self.toCheck == []:
			self.toCheck = ['b', 'd']
			self.number_toStop = 3

	# --------------------------------------------

	def getShortLE(self, d, a):
		return unpack('<H',(d)[a:a+2])[0]

	def getLongBE(self, d, a):
		return unpack('>L',(d)[a:a+4])[0]

	def printASN1(self, d, l, rl):
		type = ord(d[0])
		length = ord(d[1])
		if length&0x80 > 0: #http://luca.ntop.org/Teaching/Appunti/asn1.html,
			nByteLength = length&0x7f
			length = ord(d[2])  
			#Long form. Two to 127 octets. Bit 8 of first octet has value "1" and bits 7-1 give the number of additional length octets. 
			skip=1
		else:
			skip=0    

		if type==0x30:
			seqLen = length
			readLen = 0
			while seqLen>0:
				len2 = self.printASN1(d[2+skip+readLen:], seqLen, rl+1)
				seqLen = seqLen - len2
				readLen = readLen + len2
			return length+2
		elif type==6: #OID
			return length+2
		elif type==4: #OCTETSTRING
			return length+2
		elif type==5: #NULL
			# print 0
			return length+2
		elif type==2: #INTEGER
			return length+2
		else:
			if length==l-2:
				self.printASN1( d[2:], length, rl+1)
				return length   

	#extract records from a BSD DB 1.85, hash mode       
	def readBsddb(self, name):   
		f = open(name,'rb')
		
		#http://download.oracle.com/berkeley-db/db.1.85.tar.gz
		header = f.read(4*15)
		magic = self.getLongBE(header,0)
		if magic != 0x61561:
			print_debug('WARNING', 'Bad magic number')
			return False
		version = self.getLongBE(header,4)
		if version !=2:
			print_debug('WARNING', 'Bad version !=2 (1.85)')
			return False
		pagesize = self.getLongBE(header,12)
		nkeys = self.getLongBE(header,0x38) 

		readkeys = 0
		page = 1
		nval = 0
		val = 1
		db1 = []
		while (readkeys < nkeys):
			f.seek(pagesize*page)
			offsets = f.read((nkeys+1)* 4 +2)
			offsetVals = []
			i=0
			nval = 0
			val = 1
			keys = 0
			while nval != val :
				keys +=1
				key = self.getShortLE(offsets,2+i)
				val = self.getShortLE(offsets,4+i)
				nval = self.getShortLE(offsets,8+i)
				offsetVals.append(key+ pagesize*page)
				offsetVals.append(val+ pagesize*page)  
				readkeys += 1
				i += 4
			offsetVals.append(pagesize*(page+1))
			valKey = sorted(offsetVals)  
			for i in range( keys*2 ):
				f.seek(valKey[i])
				data = f.read(valKey[i+1] - valKey[i])
				db1.append(data)
			page += 1
		f.close()
		db = {}

		for i in range( 0, len(db1), 2):
			db[ db1[i+1] ] = db1[ i ]

		return db  

	def decrypt3DES(self, globalSalt, masterPassword, entrySalt, encryptedData ):
		#see http://www.drh-consultancy.demon.co.uk/key3.html
		hp = sha1( globalSalt+masterPassword ).digest()
		pes = entrySalt + '\x00'*(20-len(entrySalt))
		chp = sha1( hp+entrySalt ).digest()
		k1 = hmac.new(chp, pes+entrySalt, sha1).digest()
		tk = hmac.new(chp, pes, sha1).digest()
		k2 = hmac.new(chp, tk+entrySalt, sha1).digest()
		k = k1+k2
		iv = k[-8:]
		key = k[:24]

		return DES3.new( key, DES3.MODE_CBC, iv).decrypt(encryptedData)

	def extractSecretKey(self, globalSalt, masterPassword, entrySalt):

		(globalSalt, masterPassword, entrySalt) = self.is_masterpassword_correct(masterPassword)
		
		if unhexlify('f8000000000000000000000000000001') not in self.key3:
			return None
		privKeyEntry = self.key3[ unhexlify('f8000000000000000000000000000001') ]
		saltLen = ord( privKeyEntry[1] )
		nameLen = ord( privKeyEntry[2] )
		privKeyEntryASN1 = decoder.decode( privKeyEntry[3+saltLen+nameLen:] )
		data = privKeyEntry[3+saltLen+nameLen:]
		self.printASN1(data, len(data), 0)
		
		#see https://github.com/philsmd/pswRecovery4Moz/blob/master/pswRecovery4Moz.txt
		entrySalt = privKeyEntryASN1[0][0][1][0].asOctets()
		privKeyData = privKeyEntryASN1[0][1].asOctets()
		privKey = self.decrypt3DES( globalSalt, masterPassword, entrySalt, privKeyData )
		self.printASN1(privKey, len(privKey), 0)

		privKeyASN1 = decoder.decode( privKey )
		prKey= privKeyASN1[0][2].asOctets()
		self.printASN1(prKey, len(prKey), 0)
		prKeyASN1 = decoder.decode( prKey )
		id = prKeyASN1[0][1]
		key = long_to_bytes( prKeyASN1[0][3] )

		print_debug('DEBUG', 'key: %s' % repr(key))
		return key

	# --------------------------------------------
	
	# Get the path list of the firefox profiles
	def get_firefox_profiles(self, directory):
		cp = RawConfigParser()
		try:
			cp.read(os.path.join(directory, 'profiles.ini'))
		except:
			return []
		
		profile_list = []
		for section in cp.sections():
			if section.startswith('Profile'):
				if cp.has_option(section, 'Path'):
					profile_list.append(os.path.join(directory, cp.get(section, 'Path').strip()))
		return profile_list
	
	def save_db(self, userpath):
		
		# create the folder to save it by profile
		relative_path = constant.folder_name + os.sep + 'firefox'
		if not os.path.exists(relative_path):
			os.makedirs(relative_path)
		
		relative_path += os.sep + os.path.basename(userpath)
		if not os.path.exists(relative_path):
			os.makedirs(relative_path)
		
		# Get the database name
		if os.path.exists(userpath + os.sep + 'logins.json'):
			dbname = 'logins.json'
		elif os.path.exists(userpath + os.sep + 'signons.sqlite'):
			dbname = 'signons.sqlite'
		
		# copy the files (database + key3.db)
		try:
			ori_db = userpath + os.sep + dbname
			dst_db = relative_path + os.sep + dbname
			shutil.copyfile(ori_db, dst_db)
			print_debug('INFO', '%s has been copied here: %s' % (dbname, dst_db))
		except Exception,e:
			print_debug('DEBUG', '{0}'.format(e))
			print_debug('ERROR', '%s has not been copied' % dbname)
		
		try:
			dbname = 'key3.db'
			ori_db = userpath + os.sep + dbname
			dst_db = relative_path + os.sep + dbname
			shutil.copyfile(ori_db, dst_db)
			print_debug('INFO', '%s has been copied here: %s' % (dbname, dst_db))
		except Exception,e:
			print_debug('DEBUG', '{0}'.format(e))
			print_debug('ERROR', '%s has not been copied' % dbname)

	# ------------------------------ Master Password Functions ------------------------------
	
	def is_masterpassword_correct(self, masterPassword=''):
		try:
			#see http://www.drh-consultancy.demon.co.uk/key3.html
			pwdCheck = self.key3['password-check']	
			entrySaltLen = ord(pwdCheck[1])
			entrySalt = pwdCheck[3: 3+entrySaltLen]
			encryptedPasswd = pwdCheck[-16:]
			globalSalt = self.key3['global-salt']
			cleartextData = self.decrypt3DES( globalSalt, masterPassword, entrySalt, encryptedPasswd )
			if cleartextData != 'password-check\x02\x02':
				return ('', '', '')

			return (globalSalt, masterPassword, entrySalt)
		except:
			return ('', '', '')
	
	# Retrieve masterpassword
	def found_masterpassword(self):
		
		# master password entered manually
		if 'm' in self.toCheck:
			print_debug('ATTACK', 'Check the password entered manually !')
			if self.is_masterpassword_correct(self.manually_pass)[0]:
				print_debug('FIND', 'Master password found: %s' % self.manually_pass)
				return self.manually_pass
			else:
				print_debug('WARNING', 'The Master password entered is not correct')
		
		# dictionary attack
		if 'a' in self.toCheck:
			try:
				pass_file = open(self.dictionary_path, 'r')
				num_lines = sum(1 for line in pass_file)
			except:
				print_debug('ERROR', 'Unable to open passwords file: %s' % str(self.dictionary_path))
				return False
			pass_file.close()
			
			print_debug('ATTACK', 'Dictionary Attack !!! (%s words)' % str(num_lines))
			try:
				with open(self.dictionary_path) as f:
					for p in f:
						if self.is_masterpassword_correct(p.strip())[0]:
							print_debug('FIND', 'Master password found: %s' % p.strip())
							return p.strip()
			
			except (KeyboardInterrupt, SystemExit):
				print 'INTERRUPTED!'
				print_debug('DEBUG', 'Dictionary attack interrupted')
			except Exception,e:
				print_debug('DEBUG', '{0}'.format(e))

			print_debug('WARNING', 'The Master password has not been found using the dictionary attack')
		
		# 500 most used passwords
		if 'd' in self.toCheck:
			wordlist = get_dico() + constant.passwordFound
			num_lines = (len(wordlist)-1)
			print_debug('ATTACK', '%d most used passwords !!! ' % num_lines)

			for word in wordlist:
				if self.is_masterpassword_correct(word)[0]:
					print_debug('FIND', 'Master password found: %s' % word.strip())
					return word
				
			print_debug('WARNING', 'No password has been found using the default list')
		
		# brute force attack
		if 'b' in self.toCheck or constant.bruteforce:
			charset_list = 'abcdefghijklmnopqrstuvwxyz1234567890!?'
			print_debug('ATTACK', 'Brute force attack !!! (%s characters)' %  str(constant.bruteforce))
			print_debug('DEBUG', 'charset: %s' %  charset_list)

			try:
				for length in range(1, int(self.number_toStop)):
					words = product(charset_list, repeat=length)
					for word in words:
						print_debug('DEBUG', '%s' %  ''.join(word))
						if self.is_masterpassword_correct(''.join(word))[0]:
							w = ''.join(word)
							print_debug('FIND', 'Master password found: %s' % w.strip())
							return w.strip()
			except (KeyboardInterrupt, SystemExit):
				print 'INTERRUPTED!'
				print_debug('INFO', 'Dictionary attack interrupted')
			except Exception,e:
				print_debug('DEBUG', '{0}'.format(e))

			print_debug('WARNING', 'No password has been found using the brute force attack')
		return False

	# ------------------------------ End of Master Password Functions ------------------------------
	
	# main function
	def run(self, software_name = None):
		global database_find
		database_find = False

		self.manage_advanced_options()
		
		if constant.mozilla_software:
			software_name = constant.mozilla_software
		specific_path = constant.specific_path
		
		# get the installation path
		path = self.get_path(software_name)
		if not path:
			print_debug('WARNING', 'Installation path not found')
			return
		
		#Check if mozilla folder has been found
		elif not os.path.exists(path):
			print_debug('INFO', software_name + ' not installed.')
			return
		else:
			if specific_path:
				if os.path.exists(specific_path):
					profile_list = [specific_path]
				else:
					print_debug('WARNING', 'The following file does not exist: %s' % specific_path)
					return
			else:
				profile_list = self.get_firefox_profiles(path)

			pwdFound = []
			for profile in profile_list:
				print_debug('INFO', 'Profile path found: %s' % profile)
				if not os.path.exists(profile + os.sep + 'key3.db'):
					print_debug('WARNING', 'key3 file not found: %s' % self.key3)
					continue

				self.key3 = self.readBsddb(profile + os.sep + 'key3.db')
				if not self.key3:
					continue

				# check if passwords are stored on the Json format
				try:
					credentials = JsonDatabase(profile)
				except:
					database_find = False

				if not database_find:
					# check if passwords are stored on the sqlite format
					try:
						credentials = SqliteDatabase(profile)
					except:
						database_find = False
				
				if database_find:
					masterPassword = ''
					(globalSalt, masterPassword, entrySalt) = self.is_masterpassword_correct(masterPassword)
					
					# find masterpassword if set
					if not globalSalt:
						print_debug('WARNING', 'Master Password is used !') 
						masterPassword = self.found_masterpassword()
						if not masterPassword:
							continue
					
					# get user secret key
					key = self.extractSecretKey(globalSalt, masterPassword, entrySalt)
					if not key:
						continue

					# everything is ready to decrypt password
					for host, user, passw in credentials:
						values = {}
						values["URL"] = host

						# Login	
						loginASN1 = decoder.decode(b64decode(user))
						iv = loginASN1[0][1][1].asOctets()
						ciphertext = loginASN1[0][2].asOctets()
						login = DES3.new( key, DES3.MODE_CBC, iv).decrypt(ciphertext)
						# remove bad character at the end
						try:
							nb = unpack('B', login[-1])[0]
							values["Login"] = login[:-nb]
						except:
							values["Login"] = login
						
						# Password
						passwdASN1 = decoder.decode(b64decode(passw))
						iv = passwdASN1[0][1][1].asOctets()
						ciphertext = passwdASN1[0][2].asOctets()
						password = DES3.new( key, DES3.MODE_CBC, iv).decrypt(ciphertext)
						# remove bad character at the end
						try:
							nb = unpack('B', password[-1])[0]
							values["Password"] =  password[:-nb]
						except:
							values["Password"] =  password

						if len(values):
							pwdFound.append(values)

			return pwdFound
